package se.webbzon.boltzmann.game;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.Arrays;

import se.webbzon.boltzmann.debug.Debug;
import se.webbzon.boltzmann.game.environment.GameEnvironment;
import se.webbzon.boltzmann.login.AccessException;
import se.webbzon.boltzmann.login.HTTPMultipartPostRequest;
import se.webbzon.boltzmann.login.HTTPPostRequest;
import se.webbzon.boltzmann.login.HTTPResponse;
import se.webbzon.boltzmann.login.LoginAccess;
import se.webbzon.oschi01.aes.HexOutputStream;
import se.webbzon.oschi01.aes.RijndaelBuilder;
import se.webbzon.oschi01.aes.RijndaelCipher;
import se.webbzon.oschi01.aes.RijndaelMatrix;
import se.webbzon.oschi01.hash.MD5OutputStream;

public class OnlineGame extends Game {
	
	// A UTF-8 character set used for encoding/decoding strings
	Charset utf8 = Charset.forName("UTF-8");
	
	// The login access of this online game
	private final LoginAccess access;
	
	// The game id and license id associated with this online game
	private final int id, licenseId;
	
	// The revision of the game
	private int revision;
	
	/*============================================================
	Constructors
	============================================================*/

	public OnlineGame(LoginAccess access, int id, int licenseId, int revision, String name) {
		super(name);
		this.access = access;
		this.id = id;
		this.licenseId = licenseId;
		this.revision = revision;
	}
	
	/*============================================================
	Public Methods
	============================================================*/
	
	@Override public boolean hasBackup() {
		return true;
	}

	@Override public Game getBackup() {
		return new LocalGame(getFile());
	}
	
	/** Returns the id of this online game. **/
	public int getId() {
		return id;
	}

	@Override public String getName() {
		if (getFile().exists())
			return super.getName() + "\n(Local backup)";
		else
			return super.getName();
	}

	/** Returns the id of the license associated with this online game. **/
	public int getLicenseId() {
		return licenseId;
	}
	
	/** Returns the current revision of the online game. **/
	public int getRevision() {
		return revision;
	}
	
	/** Returns the current revision of the online game. **/
	public void setRevision(int revision) {
		this.revision = revision;
	}

	@Override public GameEnvironment load(GameConfiguration config) throws Exception {
		if (revision == 0)
			return createGame(config);
		
		File gameFile = getFile();
		if (gameFile != null && gameFile.exists()) {
			FileInputStream is = new FileInputStream(gameFile);
			GameEnvironment genv = new GameReader().read(this, config, is);
			is.close();
			return genv;
		} else {
			URL url = new URL("http://" + access.getDomain() + "/boltzmann/loadgame.php");
			// Encrypt game data
			RijndaelCipher cipher = new RijndaelCipher();
			cipher.setKey(RijndaelMatrix.createKey(access.getHashword(),0));
			RijndaelBuilder builder = new RijndaelBuilder();
			String dec = "game_id=" + id;
			builder.write(dec.getBytes(utf8));
			byte[] decBytes = new byte[builder.size()];
			builder.writeTo(decBytes, 0);
			RijndaelMatrix[] matrices = RijndaelMatrix.fromBytes(decBytes, 0, decBytes.length);
			cipher.encrypt(matrices);
			String enc = HexOutputStream.toHex(decBytes);
			
			return attemptLoad(url, enc, config, true);
		}
	}

	@Override public void save(GameEnvironment env) throws Exception {
		URL url = new URL("http://" + access.getDomain() + "/boltzmann/savegame.php");
		MD5OutputStream mos = new MD5OutputStream();
		new GameWriter().write(env, mos);
		// Encrypt game data
		RijndaelCipher cipher = new RijndaelCipher();
		cipher.setKey(RijndaelMatrix.createKey(access.getHashword(),0));
		RijndaelBuilder builder = new RijndaelBuilder();
		String dec =	"game_method=save" +
						"&game_id=" + id + 
						"&game_revision=" + (revision+1) + 
						"&game_md5=" + HexOutputStream.toHex(mos.sum());
		builder.write(dec.getBytes(utf8));
		byte[] decBytes = new byte[builder.size()];
		builder.writeTo(decBytes, 0);
		RijndaelMatrix[] matrices = RijndaelMatrix.fromBytes(decBytes, 0, decBytes.length);
		cipher.encrypt(matrices);
		String enc = HexOutputStream.toHex(decBytes);
		
		attemptSave(url,enc,env,true);
	}
	
	/*============================================================
	Private Methods
	============================================================*/
	
	/** Attempts to load the game configuration from the game server. **/
	private final GameEnvironment attemptLoad(URL url, String enc, GameConfiguration config, boolean refreshOnFail) throws AccessException, IOException {
		try (HTTPPostRequest req = new HTTPPostRequest(url,access)) {	
			req.addField("submitter_submit", enc);
			try (HTTPResponse res = req.send()) {
				byte[] header = new byte[3];
				res.readFully(header, 0, 3);
				if (Arrays.equals(header, new byte[] {'O', 'K', '\n'})) {
					return new GameReader().read(this, config, res.acquireInputStream());
				} else if (Arrays.equals(header, new byte[] {'N', 'O', '\n'})) {
					int code = getErrorCode(res.readLine());
					if (code == 2) { 
						if (refreshOnFail) {
							access.refresh();
							return attemptLoad(url,enc,config,false);
						} else
							throw new AccessException("Access was denied by server!");
					} else
						throw new IOException("Exception was thrown due to invalid server response!");
				} else
					throw new IOException("Exception was thrown due to invalid server response!");
			}
		}
	}
	
	/** Attempts to load the game configuration from the game server. **/
	private void attemptSave(URL url, String query, GameEnvironment env, boolean refreshOnFail) throws AccessException, IOException {
		String response = null, message = null;
		try (HTTPMultipartPostRequest req = new HTTPMultipartPostRequest(url,access)) {
			req.addField("submitter_submit", query);
			OutputStream os = req.acquireOutputStream("application/octet-stream", "game_data");
			new GameWriter().write(env, os);
			req.releaseOutputStream();
			try (HTTPResponse res = req.send()) {
				response = res.readLine();
				message = response != null ? res.readLine() : null; 
			}	
		}
		
		// Check the response
		if (response == null)
			throw new IOException("Exception was thrown due to invalid server response!");
		else if (response.equals("NO") && getErrorCode(message) == 2) {
			if (refreshOnFail) {
				access.refresh();
				attemptSave(url,query,env,false);
			} else
				throw new AccessException("Access was denied by server!");
		} else if (response.equals("OK")) {
			revision++;
		} else
			throw new IOException("Exception was thrown due to invalid server response!");
	}
	
	/** Returns the error code of an error message. **/
	private static final int getErrorCode(String message) {
		int fromIndex = message.indexOf('(') + 1;
		int endIndex = message.indexOf(')');
		return Integer.parseInt(message.substring(message.indexOf(' ', fromIndex) + 1,endIndex));
	}
	
	/*============================================================
	Private Methods
	============================================================*/
	
	/** Returns the backup file associated with this online game. **/
	private File getFile() {
		try {
			URI uri = LocalGame.class.getProtectionDomain().getCodeSource().getLocation().toURI();
			File runnableFile = new File(uri);
			File gameDirectory = new File(runnableFile.getParentFile(),"games/online");
			return new File(gameDirectory, getId() + "_" + getRevision() + ".bass");
		} catch (URISyntaxException e) {
			Debug.report(e);
			return null;
		}
	}

}
